package com.siyuan.blog.web.storage;
import com.google.common.collect.ImmutableMap;
import com.siyuan.blog.common.util.DownloadView;
import com.siyuan.blog.common.vo.JSONResponse;
import org.apache.commons.io.output.NullOutputStream;
import org.im4java.core.*;
import org.im4java.process.OutputConsumer;
import org.ponly.fs.FileSystem;
import org.ponly.fs.util.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.ServletContextAware;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.text.MessageFormat;
import java.util.Date;
import java.util.List;

import static org.ponly.fs.util.Throwables.rethrowRuntimeException;


/**
 *   文件上传下载统一接口
 */
@Controller
@RequestMapping("/storage")
public class StorageController implements ServletContextAware {
    private static final Logger LOG = LoggerFactory.getLogger(StorageController.class);
    public static final String DEFAULT_FILE_MIME_TYPE = "application/octet-stream";
    public static final String DEFAULT_IMAGE_MIME_TYPE = "image/jpeg";
    public static final int DEFAULT_PER_DAY_FOLDERS = 12; // 每天生成几个目录
    public static final String DATE_DIR_FMT = "%1$tY/%1$tm/%1$td/%2$04d";   // 2014/02/27/0001

    public static final String ENCODE_PREFIX = "FLS";
    public static final String ENCODE_SEPARATOR = "!!";
    public static final String SIZE_SEPARATOR = "x";                // 文件尺寸分隔符
    public static final String THUMB_NAME_FMT = "{0}_{1}x{2}.{3}";  // 缩略图命名规则

    private List<String> allowSizeList;                             // 允许的缩略图尺寸
    private boolean storeThumbnail = true;

    private int perDayFolders = DEFAULT_PER_DAY_FOLDERS;

    private ServletContext servletContext;
    @Autowired
    private FileSystem fileSystem;


    @RequestMapping("/demo")
    public String demo() {
        return "upload/demo";
    }

    /**********************************************
     *                 文件操作-Begin               *
     * *******************************************/

    /**
     *  通用上传文件
     * @param multipart
     * @return
     * @throws IOException
     */
    @ResponseBody
    @RequestMapping(value = "files/ul", method = RequestMethod.POST)
    public JSONResponse upload(@RequestPart("file") MultipartFile multipart) throws IOException {
        JSONResponse message;
        if (multipart == null || multipart.isEmpty()) {
            message = JSONResponse.fail("无效的文件, 文件不能为空");
        }
        String name = multipart.getOriginalFilename();
        InputStream inputStream = multipart.getInputStream();
        String path = this.store(name, true, inputStream,true);
        message = JSONResponse.success(ImmutableMap.<String, Object>of("path", path));
        return message;
    }

    /**
     * 内联方式获取文件资源
     * @param fid
     * @param request
     * @param response
     * @throws IOException
     */
    @RequestMapping("files/{fid:.*}") // spring 默认会去掉后缀, 使用 .* 避免
    public void inline(@PathVariable("fid") String fid, HttpServletRequest request, HttpServletResponse response) throws IOException {
        final InputStream is = this.readAsStream(fid,false);
        if (null != is) {
            String mimeType = getMimeType(fid);
            if (StringUtils.hasText(mimeType)) {
                response.setContentType(mimeType);
            }

            new DownloadView("", false) {
                @Override
                protected InputStream getResourceAsStream() {
                    return is;
                }
            }.sendTo(request, response);

        } else {
            // writer.write("您下载的文件不存在")
            response.sendError(HttpServletResponse.SC_NOT_FOUND, "File Not Find");
        }
    }

    /**
     * 文件下载
     * @param fid
     * @param filename
     * @param responseContentType
     * @param request
     * @param response
     * @throws IOException
     */
    @RequestMapping("files/dl/{fid:.*}")
    // @RequestMapping("dl/{hash}/{filename}")
    public void download(@PathVariable("fid") String fid, String filename, String responseContentType, HttpServletRequest request, HttpServletResponse response) throws IOException {
        final InputStream is = this.readAsStream(fid, false);
        if (null != is) {
            filename = StringUtils.hasText(filename) ? filename : fid;
            // response.setHeader("Content-MD5", hash);

            // 如果 contentType 为空, 尝试从 filename 中获取
            if (!StringUtils.hasText(responseContentType)) {
                responseContentType = getMimeType(filename);
            }

            // 如果 contentType 仍然为空, 尝试从标识中获取
            if (!StringUtils.hasText(responseContentType)) {
                responseContentType = getMimeType(fid);
            }

            responseContentType = StringUtils.hasText(responseContentType) ? responseContentType : DEFAULT_FILE_MIME_TYPE;

            new DownloadView(filename, responseContentType) {
                @Override
                protected InputStream getResourceAsStream() {
                    return is;
                }
            }.sendTo(request, response);
        } else {
            response.sendError(HttpServletResponse.SC_NOT_FOUND, "File Not Find");
        }
    }


    /**
     * 获取图片资源   /images/aa.jpg
     */
    @RequestMapping("images/{path:.*}")
    public void view(@PathVariable("path") String path, boolean download, HttpServletRequest request, HttpServletResponse response) throws IOException {
        final InputStream is = this.readAsStream(path,true);

        if (null != is) {
            String type = getMimeType(path);
            type = StringUtils.hasText(type) ? type : DEFAULT_IMAGE_MIME_TYPE;

            new DownloadView("", type, download) {
                @Override
                protected InputStream getResourceAsStream() {
                    return is;
                }
            }.sendTo(null, response);
        }
    }

    /**********************************************
     *                 操作-END               *
     * *******************************************/

    /**
     * 存储
     * @param originalFilename
     * @param generateNewName
     * @param in
     * @param override
     * @return
     * @throws IOException
     */
    public String store(String originalFilename, boolean generateNewName, InputStream in, boolean override) throws IOException {
        String dir = getDailyRollingDirectory();
        Path target = Path.get(dir);
        String filename = originalFilename;

        if (null == originalFilename || generateNewName) {
            String suffix = null != originalFilename ? FileUtils.getExtension(originalFilename, true) : "";
            filename = generateFilename() + suffix;
        }
        target = target.resolve(filename);

        try {
            return encodePath(doStore(target.getPath(), in, override));
        } catch (IOException e) {
            LOG.warn("store failure: {}", e);
            throw new IOException(e);
        } finally {
            IOUtils.close(in);
        }
    }

    protected String doStore(String file, InputStream in, boolean override) throws IOException {
        String path = Path.get(file).getPath();
        OutputStream out = doCreate(file, override);
        if (null != out) {
            IOUtils.flow(in, out, true, true);
        }
        return path;
    }

    /**
     * 创建输出流
     * @param file
     * @param override
     * @return
     */
    protected OutputStream doCreate(String file, boolean override) {
        FileSystem fs = getRequiredFileSystem();
        return fs.create(file, override);
    }

    /**
     * 生成文件名
     * @return
     */
    protected String generateFilename() {
        byte[] bytes = Bytes.toBytes(new Date().getTime() + Randoms.next(0, Long.MAX_VALUE));
        return Hex.encode(bytes);
    }

    /**
     * 获取文件夹路径
     * @return
     */
    protected String getDailyRollingDirectory() {
        Date date = new Date();
        int random = (int) (date.getTime() % getPerDayFolders());
        return String.format(DATE_DIR_FMT, date, random);
    }

    /**
     * 获取FileSystem 对象
     * @return
     */
    protected FileSystem getRequiredFileSystem() {
        if (null == fileSystem) {
            throw new IllegalStateException("No FileSystem found: no FileSystem configure?");
        }
        return fileSystem;
    }

    public int getPerDayFolders() {
        return perDayFolders;
    }


    /**
     * encode given file path
     *
     * @param path
     * @return
     */
    private String encodePath(String path) {
        if (path.startsWith(ENCODE_PREFIX)) {
            return path;
        }
        path = !path.endsWith("/") ? path : path.substring(0, path.length() - 1);

        int i = path.lastIndexOf("/");
        if (-1 < i) {
            path = Base64.encodeToUrlSafeString(Bytes.toBytes(path.substring(0, i))) + ENCODE_SEPARATOR + path.substring(i + 1);
        }
        return ENCODE_PREFIX + path;
    }

    /**
     * decode {@link #encodePath(String)}
     *
     * @param encodedPath
     * @return
     */
    private String decodePath(String encodedPath) {
        int i = encodedPath.indexOf(ENCODE_SEPARATOR);
        if (-1 < i && encodedPath.startsWith(ENCODE_PREFIX)) {
            String fname = encodedPath.substring(i + 2);
            encodedPath = encodedPath.substring(ENCODE_PREFIX.length(), i); // ignore F
            encodedPath = Bytes.toString(Base64.decode(encodedPath, true)) + "/" + fname;
        }
        return encodedPath;
    }

    protected String getMimeType(String filename) {
        return servletContext.getMimeType(filename);
    }

    @Override
    public void setServletContext(ServletContext servletContext) {
        this.servletContext = servletContext;
    }

    /**
     * 读取输入流
     * @param id
     * @return
     */
    public final InputStream readAsStream(String id, boolean tabloid) {
        return tabloid ? doImgOpen(decodePath(id)) : doOpen(decodePath(id));
    }

    /**
     * 获取文件输入流
     * @param file
     * @return
     */
    protected InputStream doOpen(String file) {
        FileSystem fs = getRequiredFileSystem();
        return fs.open(file);
    }

    public InputStream doImgOpen(String file) {
        file = decodePath(file);        // 存储路径

        InputStream in = this.doOpen(file);
        if (null != in) {       // exists
            return in;
        }

        // 可能是需要生成缩略图的请求
        final Path path = Path.get(file);
        final String parentPath = path.getParent().getPath();       // 请求的文件路径
        final String filename = path.getName();                           // 请求的文件名称

        int origNameLength = getOriginalNameLength(filename);      // 原始图片名称
        if (origNameLength == filename.length()) {                 // 不包含尺寸信息
            return null;
        }

        String origName = filename.substring(0, origNameLength);            // 未缩放的文件名
        String suffix = FileUtils.getExtension(origName, true);             // 文件后缀

        // 提取缩略尺寸名字
        String sizeStr;
        if (filename.endsWith(suffix)) {
            sizeStr = filename.substring(origNameLength + 1, filename.length() - suffix.length());
        } else {
            sizeStr = filename.substring(origNameLength + 1);
        }

        if (null != allowSizeList && allowSizeList.size() > 0 && !allowSizeList.contains(filename)) {
            LOG.debug("ignore invalid thumbnail: {} {}", origName, filename);
            return null;
        }

        // 尝试获取所缩略图大小
        String[] size = sizeStr.split(SIZE_SEPARATOR);
        if (2 != size.length) {
            return null;
        }
        boolean ratio = true; //是否保持等比
        if(size[1].endsWith("!")) {
            ratio = false;
            size[1] = size[1].substring(0,size[1].length()-1);
        }
        try {
            int width = Integer.parseInt(size[0]);
            int height = Integer.parseInt(size[1]);
            String thumbnail = getThumbnailName(origName, width, height);
                String thumbnailPath = Path.get(sizeStr, parentPath).resolve(thumbnail).getPath();
                in = this.doOpen(thumbnailPath);
                if (null != in) {
                    // 缩略图已经存在
                    return in;
                }


            // 读取原始文件
            in = this.doOpen(Path.get(parentPath, origName).getPath());
            if (null == in) {
                LOG.debug("ignore thumbnail, original file is not exists: {}", origName);
                return null;
            }

            try {
                // write original file to disk
                File origFile = File.createTempFile("ponly-ils", suffix);
                IOUtils.flow(in, new FileOutputStream(origFile), true, true);

                // generate thumbnail
                File thumb = new File(origFile.getAbsolutePath() + "." + sizeStr + suffix);
                LOG.debug("generate thumbnail {}", thumbnail);
                // 生成缩略图
                generateThumb(origFile, thumb, width, height,ratio);

                if (storeThumbnail) {
                    this.doStore(thumbnailPath, new FileInputStream(thumb),true);
                }

                // origFile.delete();
                return new FileInputStream(thumb);
            } catch (Exception ex) {
                // 如果异常，返回原图
                LOG.warn("generate thumbs failure {}", ex);
                return this.doOpen(Path.get(parentPath, origName).getPath());
            }
        } catch (Exception e) {
            return null;
        }
    }

    private int getOriginalNameLength(String thumbName) {
        int len = thumbName.length();
        char[] chars = thumbName.toCharArray();

        for (int i = 0; i < chars.length - 1; i++) {
            char cur = chars[i];
            char next = chars[i + 1];

            if ('_' == cur && Character.isDigit(next)) {
                len = i;
            }
        }
        return len;
    }

    private String getThumbnailName(String origName, int w, int h) {
        String suffix = FileUtils.getExtension(origName, false);
        return MessageFormat.format(THUMB_NAME_FMT, origName, w, h, suffix);
    }

    /**
     * 生成缩略图
     *
     * @param source
     * @param thumb
     * @param width
     * @param height
     */
    protected void generateThumb(File source, File thumb, int width, int height, boolean ratio) {
        try {
            try {
                if (gmIsValid) {
                    IMOperation op = new IMOperation();
                    // op.resize(width, height, "^").gravity("center").extent(width, height).quality(90d);
                    op.resize(width, height, ratio ? "" : "!").gravity("center").quality(90d);
                    op.addImage().addImage();

                    runIMConvertOps(op, source.getAbsolutePath(), thumb.getAbsolutePath());
                    return;
                }
            } catch (Exception e) {
                LOG.warn("GraphicsMagic execute fail: ", e);
            }

            String format = FileUtils.getExtension(thumb.getName(), false);
            BufferedImage orignal = Images.read(source);
            Image result = Images.scale(orignal, width, height, Image.SCALE_FAST | Image.SCALE_SMOOTH);
            Images.write(result, format, thumb);
        } catch (IOException e) {
            rethrowRuntimeException(e);
        }
    }


    /**
     * 运行IM4j 命令
     * @param ops
     * @param args
     * @throws InterruptedException
     * @throws IOException
     * @throws IM4JavaException
     */
    private void runIMConvertOps(IMOps ops, Object... args) throws InterruptedException, IOException, IM4JavaException {
        ConvertCmd cmd = new ConvertCmd(true);
        cmd.run(ops, args);
    }







    private static boolean gmIsValid = false;
    private static boolean cmykSupported = false;

    static {
        try {
            // 测试 graphics magic
            ImageCommand gm = new ImageCommand("gm");
            gm.setOutputConsumer(NullOutputConsumer.INSTANCE);
            gm.run(new Operation().addRawArgs("-help"));

            gmIsValid = true;
            LOG.info("find GM (Graphics Magick)...");
        } catch (Exception ignore) { /* ignore */ }

        try {
            // 尝试 twelvemonkeys imageio (cmyk jpeg support)
            Thread.currentThread().getContextClassLoader().loadClass("com.twelvemonkeys.imageio.plugins.jpeg.JPEGImageReaderSpi");

            cmykSupported = true;
            LOG.info("find \"twelyemonkeys imageio-jpeg\" plugins ...");
        } catch (ClassNotFoundException ignore) { /* ignore */ }

        if (!gmIsValid && !cmykSupported) {
            LOG.warn("Application is not support CMYK images (JPEG),");
            LOG.warn("because can't find \"twelyemonkeys imageio-jpeg\" plugins or \"GM (Graphics Magick)\",");
            LOG.warn("you can find them here, imageio-jpeg  https://github.com/haraldk/TwelveMonkeys, GM: http://www.graphicsmagick.org");
        }
    }

    private static class NullOutputConsumer implements OutputConsumer {
        public static final NullOutputConsumer INSTANCE = new NullOutputConsumer();

        @Override
        public void consumeOutput(InputStream inputStream) throws IOException {
            IOUtils.flow(inputStream, NullOutputStream.NULL_OUTPUT_STREAM, true, true);
        }
    }

}
